<?php

/**
 * km::util::Uploader
 *
 * @author vbolshov
 */

namespace km\util;

use \Closure;

class Uploader {
	const ALL = -1;
	/**
	 * @var Uploader_Entry[]
	 */
	private $files = array();
	/**
	 * @var string[]
	 */
	private $moved = array(), 
		$failed_postprocessing = array();
	/**
	 * @var int
	 */
	private $required;
	/**
	 * @var Closure[]
	 */
	private $listeners = array(
		'before_move' => array(),
		'after_move' => array(),
	);
	/**
	 * @param string[] $keys
	 * @param int $required
	 * @throws Uploader_Exception
	 */
	function __construct($keys, $required = 0)
	{
		$this->required = $required;
		$existing = $added = 0;
		foreach ((array) $keys as $key) {
			$new = $this->add($key);
			for ($i = 0; $i < $new; ++$i)
			{
				if ($this->files[$added + $i]->exists())
				{
					$existing++;
				}
			}
			$added += $new;
		}
		
		if ((($this->required == self::ALL) && ($added > $existing)) || 
			($this->required > $existing))
		{
			// throwing exceptions in constructor turns to be design mistake
			// @todo rewrite this part of code
			throw new Uploader_MissingRequiredFilesException();
		}
	}
	/**
	 * Specify accepted filename extension(s)
	 * @param string | string[] $extension
	 * @param string $error_message
	 * @return void
	 */
	function acceptExtension($extension, $error_message = 'File %s has unacceptable extension')
	{
		$valid_extensions = array_map('strtolower', (array) $extension);
		$this->onBeforeMove(function(Uploader_Entry $entry) use ($valid_extensions) {
			$test_extension = strtolower(pathinfo($entry->getName(), PATHINFO_EXTENSION));
			return in_array($test_extension, $valid_extensions);
		}, $error_message);
	}
	/**
	 * Specify accepted mime type(s)
	 * @param string | string[] $type
	 * @param string $error_message
	 * @return void
	 */
	function acceptType($type, $error_message = 'File %s has unacceptable MIME type')
	{
		$valid_types = array_map('strtolower', (array) $type);
		$this->onBeforeMove(function(Uploader_Entry $entry) use ($valid_types) {
			return in_array(strtolower($entry->getType()), $valid_types);
		}, $error_message);
	}
	/**
	 * Specify accepted filename pattern
	 * @param string $regexp
	 * @param string $error_message
	 * @return void
	 */
	function acceptPattern($regexp, $error_message = 'File %s has unacceptable name')
	{
		$this->onBeforeMove(function(Uploader_Entry $entry) use ($regexp) {
			return (bool) preg_match($regexp, $entry->getName());
		}, $error_message);
	}
	
	/**
	 * Add 'before_move' listener
	 * @param Closure $listener
	 * @param string $error_message
	 */
	function onBeforeMove(Closure $listener, $error_message = 'File %s failed preliminary check')
	{
		$this->listeners['before_move'][] = array($listener, $error_message);
	}
	/**
	 * Add 'after_move' listener
	 * @param Closure $listener
	 * @param string $error_message
	 */
	function onAfterMove(Closure $listener, $error_message = 'File %s failed post-processing')
	{
		$this->listeners['after_move'][] = array($listener, $error_message);
	}
	/**
	 * @param string $dir
	 * @return bool
	 * @throws Uploader_UploadFailedException
	 */
	function move($dir)
	{
		foreach ($this->files as $entry)
		{
			if ($entry->isError()) {
				// no events on uploads that are suerly erroneous
				$entry->move('');
			}

			if ($this->fireEvent('before_move', $entry, $dir))
			{
				$filename = $entry->move($dir);
				$this->moved[] = $filename;
				if (! $this->fireEvent('after_move', $entry, $dir))
				{
					$this->failed_postprocessing[] = $filename;
				}
			} else {
				$this->failed_precheck[] = $entry->getName();
			}
		}
		return count($this->moved);
	}
	/**
	 * delete moved files
	 */
	function rollback()
	{
		return $this->delete($this->moved);
	}
	function deleteFilesThatFailedPostprocessing()
	{
		return $this->delete($this->failed_postprocessing);
	}
	private function delete($files)
	{
		$ret = 0;
		foreach ($files as $filename)
		{
			if (is_file($filename))
			{
				unlink($filename);
				$ret++;
			}
		}
		return $ret;
	}
	/**
	 * @param string $event
	 * @return bool
	 */
	private function fireEvent($event, Uploader_Entry $entry, $dir)
	{
		foreach ($this->listeners[$event] as $listener_spec)
		{
			list($listener, $error_message) = $listener_spec;
			if (false === $listener->__invoke($entry, $dir))
			{
				throw new Uploader_UploadFailedException(sprintf($error_message, $entry->getName()));
			}
		}
		
		return true;
	}
	/**
	 * @return Uploader_Entry[]
	 */
	function export()
	{
		return $this->files;
	}
	/**
	 * @param int $index
	 * @return Uploader_Entry
	 * @throws Uploader_Exception
	 */
	function getEntry($index)
	{
		if (! isset($this->files[$index]))
		{
			throw new Uploader_Exception();
		}
		return $this->files[$index];
	}
	
	/**
	 * @param string $key
	 * @return int number of files added
	 * @throws Uploader_InvalidKeyException
	 */
	private function add($key)
	{
		if (! isset($_FILES[$key]))
		{
			throw new Uploader_InvalidKeyException($key);
		}
		$entry = $_FILES[$key];
		if (is_array($entry['name']))
		{
			$ret = 0;
			foreach ($entry['name'] as $i => $name)
			{
				$this->addOne($key, $i, $name, $entry['type'][$i], $entry['tmp_name'][$i], $entry['error'][$i]);
				$ret++;
			}
			return $ret;
		} else {
			$this->addOne($key, 0, $entry['name'], $entry['type'], $entry['tmp_name'], $entry['error']);
			return 1;
		}
	}
	
	private function addOne($key, $index, $name, $type, $tmp_name, $error)
	{
		$ue = new Uploader_Entry();
		$ue->setKey($key);
		$ue->setIndex($index);
		$ue->setName($name);
		$ue->setType($type);
		$ue->setTmpName($tmp_name);
		$ue->setErrorCode($error);
		$this->files[] = $ue;
	}
}